package com.dd.ms.auth.service.impl;

import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;;

/**
 * @Author liuxianmeng
 * @CreateTime 2025/7/10 15:25
 * @Description 令牌黑名单管理
 */
@Service
public class TokenBlacklistService {

    // 使用 ConcurrentHashMap 存储黑名单中的令牌及其过期时间戳
    private final ConcurrentHashMap<String, Long> blacklist = new ConcurrentHashMap<>();
    // 黑名单键的前缀
    private static final String BLACKLIST_PREFIX = "jwt:blacklist:";
    // 黑名单的最大容量
    private static final int MAX_BLACKLIST_SIZE = 10000;
    // 定期清理过期令牌的间隔时间（秒）
    private static final long CLEANUP_INTERVAL = 60 * 5;

    // 定时任务执行器，用于定期清理过期令牌
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    /**
     * 构造函数，初始化定时任务，定期清理过期令牌
     */
    public TokenBlacklistService() {
        // 定期执行清理任务，首次延迟 CLEANUP_INTERVAL 秒后开始执行，之后每隔 CLEANUP_INTERVAL 秒执行一次
        scheduler.scheduleAtFixedRate(this::cleanupExpiredTokens, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.SECONDS);
    }

    /**
     * 将令牌加入黑名单
     * @param token 令牌
     * @param expirationTime 令牌的过期时间（毫秒）
     */
    public void revokeToken(String token, long expirationTime) {
        long currentTime = System.currentTimeMillis(); // 当前时间（毫秒）
        long ttl = (expirationTime - currentTime) / 1000; // 令牌的剩余有效时间（秒）

        if (ttl > 0) {
            // 计算令牌的哈希值并拼接前缀作为键
            String key = BLACKLIST_PREFIX + sha256Hex(token);
            // 计算令牌的过期时间戳
            long expirationTimestamp = currentTime + ttl * 1000;

            // 如果黑名单已满，清理最早添加的过期令牌
            if (blacklist.size() >= MAX_BLACKLIST_SIZE) {
                cleanupExpiredTokens();
            }

            // 将令牌及其过期时间戳存入黑名单
            blacklist.put(key, expirationTimestamp);
        }
    }

    /**
     * 检查令牌是否在黑名单中
     *
     * @param token 令牌
     * @return 如果令牌在黑名单中且未过期，返回 true；否则返回 false
     */
    public boolean isTokenRevoked(String token) {
        // 计算令牌的哈希值并拼接前缀作为键
        String key = BLACKLIST_PREFIX + sha256Hex(token);
        // 获取令牌的过期时间戳
        Long expirationTime = blacklist.get(key);
        if (expirationTime == null) {
            // 如果黑名单中没有该令牌，返回 false
            return false;
        }
        long currentTime = System.currentTimeMillis(); // 当前时间（毫秒）
        if (currentTime >= expirationTime) {
            // 如果当前时间大于或等于过期时间戳，说明令牌已过期
            // 从黑名单中移除该令牌
            blacklist.remove(key);
            return false;
        }
        // 如果令牌在黑名单中且未过期，返回 true
        return true;
    }

    /**
     * 定期清理过期的令牌
     */
    private void cleanupExpiredTokens() {
        long currentTime = System.currentTimeMillis(); // 当前时间（毫秒）
        // 遍历黑名单，移除所有已过期的令牌
        blacklist.entrySet().removeIf(entry -> entry.getValue() <= currentTime);
    }

    /**
     * 使用 SHA-256 算法计算字符串的哈希值，并将其转换为十六进制字符串
     * @param input 输入字符串
     * @return 十六进制哈希值
     */
    private String sha256Hex(String input) {
        try {
            // 获取 SHA-256 消息摘要实例
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            // 计算输入字符串的哈希值
            byte[] hash = digest.digest(input.getBytes());
            // 将哈希值转换为十六进制字符串
            StringBuilder hexString = new StringBuilder(2 * hash.length);
            for (int i = 0; i < hash.length; i++) {
                String hex = Integer.toHexString(0xff & hash[i]);
                if (hex.length() == 1) {
                    // 如果是单个字符，前面补 0
                    hexString.append('0');
                }
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            // 如果找不到 SHA-256 算法，抛出运行时异常
            throw new RuntimeException("SHA-256 algorithm not found", e);
        }
    }

    /**
     * 对象销毁时停止定时任务
     * @throws Throwable 如果发生异常
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        // 停止定时任务执行器
        scheduler.shutdown();
    }
}
